﻿/*
 * MapBase
 * 
 * initial implementation : 20.10.2008
 *
 * Copyright (c) Adam Sida, asida@centrum.cz.
 * All rights reserved.
 * 
 * for license see Petra.Gis.License.txt
 */
using System;
using System.Collections.Generic;
using System.Runtime.Serialization;
using Petra.Gis;
using Petra.Gis.Geometry.Support;
using Petra.Gis.ReferenceSystemServices;
using Petra.Gis.ReferenceSystemServices.Transformations;
using Petra.Gis.RenderingServices.PlanarBase.Support;


namespace Petra.Gis.RenderingServices.PlanarBase 
{

  /// <summary>
  /// represents simple 2d planar (flat) map, that can display gis data in cartesian orthogonal graph.
  /// Map must have at least 2 axes named X,Y with perpendicular orientation (North, South, East, West combination)
  /// Map must have assigned projected coordinate system.
  /// Projection coordinates are inside specific map implementation transformed into output map coordinates.
  /// Source data can be anything that supports transformation into map coordinate system
  /// map coordinate system can be either:
  /// planar orthogonal cartesian 
  /// or geographic coords when orthogonal planar projection is applied
  /// no map drawing or rendering methods implemented here
  /// </summary>
  public abstract partial class MapBase : KeepRelationWith<MapLegend>
  {
    //protected MapLegend _legend;
    //protected RendererBase _renderer;
    
    //protected bool _useSpatialFilter;
    
    protected double _metersPerDisplayPaperUnit;
    protected double _metersPerSrsStorageUnit;


    #region ctor

    /// <summary>
    /// xaml constr.
    /// </summary>
    protected MapBase() 
    {
      this.KeepNorthUp = true;
      //create map legend
      //_legend = createMapLegend();

      //prepare cache for used transformations
      _srsTransformCache = new SRSTransformCache(this);
    }





    protected MapBase(float width, float height, ProjectedCoordinateSystem mapCrs, double nominalMapScale, MapPaperUnit paperOrDisplayUnit)
      :this()
    {
      this.Width = width;
      this.Height = height;

      this.ProjectedCoordinateSystem = mapCrs;

      //map scale
      this.NominalScale = nominalMapScale;

      //_renderer = new RendererBase(this);

      this.DisplayPaperUnits = paperOrDisplayUnit;

    }





    protected MapBase(float width, float height, ProjectedCoordinateSystem mapCrs, double nominalMapScale, MapPaperUnit paperOrDisplayUnit, BoundingBox bounds)
      :this(width, height, mapCrs, nominalMapScale, paperOrDisplayUnit)
    {

      //map extent
      if (bounds == null)
        throw new ArgumentException("bounds null");

      //_extent = new MapExtent(this, bounds);

    }





    protected MapBase(float width, float height, ProjectedCoordinateSystem mapCrs, double nominalMapScale, MapPaperUnit paperOrDisplayUnit, string mapTitle)
      : this(width, height, mapCrs, nominalMapScale, paperOrDisplayUnit)
    {


      //map extent
      if (_mapCRS.DefaultEnvelope != null)
        //_extent = new MapExtent(this, _mapCRS.DefaultEnvelope);
        this.ProjectionExtent = _mapCRS.DefaultEnvelope;
      else
        //no any map extent supplied, create temporary
        //_extent = new MapExtent(this, 0, 0, 1, 1);
        this.ProjectionExtent = new BoundingBox(0, 0, 1, 1);
    }


    #endregion ctor


    //protected virtual MapLegend createMapLegend()
    //{
    //  return new MapLegend(this);
    //}





    //protected string _mapName;

    /// <summary>
    /// get/set namee for current instance of Map
    /// </summary>
    public string Name
    { get; set; }






    protected MapPaperUnit _paperOrDisplayUnit;

    /// <summary>
    /// returns or sets current map display or paper units
    /// such as px, pt, mm, cm, inch,...
    /// </summary>
    public MapPaperUnit DisplayPaperUnits
    {
      get { return _paperOrDisplayUnit; }
      set 
      {

        // prepare map resolution...
        // default is 120 dpi for fast render
        // if pixel and no preference then only 96 dpi
        if (_paperOrDisplayUnit == MapPaperUnit.px)
        {
          //todo: load from system preferences
          _pixelPerInch = 96;
        }


        //if display unit (pixel), resolve pixel size
        if (value == MapPaperUnit.px)
        {
          //todo: load dpi def from preferences...
          //now set default 96 dpi
          _metersPerDisplayPaperUnit = 1.0 / 3780;
        }
        else
          _metersPerDisplayPaperUnit = value.MetersPerUnit;

        _paperOrDisplayUnit = value;

        adjustMap();

      }
    }




    protected int _pixelPerInch = 120;

    /// <summary>
    /// defines map resolution in pixels per inch for grid data and 
    /// also for screen to projection precision.
    /// Default value 120 ppi
    /// </summary>
    public int Resolution
    {
      get { return _pixelPerInch; }
      set
        {
          if (value <= 60 || value >= 5000)
            throw new ArgumentException("invalid pixel Per Inch value, must be 60 - 5000");

          _pixelPerInch = value;
        }
    }





    /// <summary>
    /// get/set property that determines whether map range
    /// is also used as spatial filter for GIS data.
    /// </summary>
    public bool ApplyMapRangeAsSpatialFilter
    { get; set; }



    //must implement in inherited
    //protected abstract MapLegend mapLegend { get; }



    protected abstract MapLegend mapLegend { get; set; }
    //{
    //  get { return this.Legend; }
    //  set
    //  {
    //    this.Legend = value;
    //  }
    //}



    //private MapLegend _legend;

    /// <summary>
    /// get/set Legend for Map. Each instance of map is driven by its legend.
    /// Legend can't be null, instead empty legend must be provided.
    /// </summary>
    public MapLegend Legend
    {
      get { return mapLegend; }
      set
      {
        if (value == null)
          throw new ArgumentException("null is invalid for legend");

        mapLegend = value;
        //notify back legend that is attached to map
        //can do some action...
        value.AttachToMap(this);

      }
    }





    protected SRSTransformCache _srsTransformCache;

    /// <summary>
    /// Coordinate system transform cache for map.
    /// When displayed data are stored in various coordinate systems
    /// this property keeps all used transforms
    /// from data source coordinate system into map projection
    /// </summary>
    public virtual SRSTransformCache SRSTransformCache
    {
      get { return _srsTransformCache; }
    }





    //public RendererBase CreateRenderer(System.Type geometryType)
    //{
    //  return _renderer;
    //}




    //protected abstract RendererBase mapCreateRenderer(System.Type geometryType);





    protected ProjectedCoordinateSystem _mapCRS;


    /// <summary>
    /// get/set current map projection (ProjectedCoordinateSystem)
    /// </summary>
    public ProjectedCoordinateSystem ProjectedCoordinateSystem
    {
      get { return _mapCRS; }
      set 
      {
        if (value == null)
          throw new ArgumentException("BaseMap.ProjectedCoordinateSystem.set value null");

        if (!value.EqualParams(_mapCRS, CsCompareType.CompareAllParams) && _mapCRS != null)
        {
          //clear transform cache
          //_srsTransformCache.Clear();
          //todo: clear output map or reload all data !!
        }

        Axis X = getAxis(value.Axes, "X");
        Axis Y = getAxis(value.Axes, "Y");

        if (X == null || Y == null)
          throw new ArgumentException("invalid projected coordinate system, "+
                                        "at least 2 axes named X and Y must be defined");

        if (!hasNorth(value))
          this.KeepNorthUp = false;

        _mapCRS = value;

        // if map projection storage units is angular, then compute
        // radians per meter from defined ellipsoid
        if (_mapCRS.Unit is AngularUnit)
        {
          _metersPerSrsStorageUnit = Math.Sin(((AngularUnit)_mapCRS.Unit).RadiansPerUnit) * (
                _mapCRS.GeographicCoordinateSystem.Datum.Ellipsoid.SemiMajorAxis *
                _mapCRS.GeographicCoordinateSystem.Datum.Ellipsoid.AxisUnit.MetersPerUnit);
        }
        else
          _metersPerSrsStorageUnit = ((DistanceUnit)_mapCRS.Unit).MetersPerUnit;

        adjustMap();
      }
    }





    protected BoundingBox _extent;

    /// <summary>
    /// get/set current map projection extent in projection coordinates
    /// </summary>
    public BoundingBox ProjectionExtent
    {
      get { return _extent; }
      set
      {
        if (value == null)
          throw new ArgumentException("invalid null reference extent", "extent");

        _extent = value;

        adjustMap();
      }
    }






    protected RenderTransformMatrix _transMatrix;

    protected abstract RenderTransformMatrix mapRenderTransform { get; }
    //{
    //  get { return _transMatrix; }
    //  set { _transMatrix = value; }
    //}





    //this must be implemented in inherited
    //protected abstract object mapDraw();


    ////empty function for this abstract class
    //public object draw()
    //{ 
    //  return null; //for now...
    //}




    private float _width;

    /// <summary>
    /// get/set current map width in map render units
    /// </summary>
    public virtual float Width
    {
      get { return _width; }
      set
      {
        if (value <= 0)
          throw new ArgumentException("invalid map width specified");

        _width = value;
        adjustMap();
        refreshMapContent();
      }
    }




    private float _height;

    /// <summary>
    /// get/set current map height in map render units
    /// </summary>
    public virtual float Height
    {
      get { return _height; }
      set
      {
        if (value <= 0)
          throw new ArgumentException("invalid map width specified");

        _height = value;
        adjustMap();
        refreshMapContent();
      }
    }



    double _nominalScale;

    /// <summary>
    /// get/set current map nominal scale. In this scale all styles defined
    /// in legend appears on map as defined, eg. 3px width line is 3px in nominal scale
    /// but is 1.5px in 2x NominalScale
    /// </summary>
    public double NominalScale
    {
      get { return _nominalScale; }
      set
      {
        if (value < 1)
          throw new ArgumentException("invalid scale: " + value.ToString());
        _nominalScale = value;

        //refresh map content...
        refreshMapContent();
      }
    }



    /// <summary>
    /// forces all renderers to get spatial data and transform them into visible map
    /// </summary>
    protected void refreshMapContent()
    {
      //render in bottm to top order !!
      for (int i = this.Legend.DisplayOrder.Count - 1; i >= 0; i--)
        this.Legend.DisplayOrder[i].RenderData();

    }





    //protected bool _keepNorthUp = true;


    /// <summary>
    /// if coordinate system provides info about north position eg. AXIS["Y",SOUTH]
    /// and KeepNorthUp = true, map prepares transform that forces north to top edge of map
    /// when no information about north in Coordinate system, this value is ignored and map 
    /// is shown in coordinate system default orientation where:
    /// X of coordinate system growths right on screen
    /// and Y of coordinate system growths bottom directions.
    /// Default value is true.
    /// </summary>
    public bool KeepNorthUp
    { get; set; }




    protected double _scale = 0;


    /// <summary>
    /// returns fraction part of scale
    /// eg. scale is 1 : 200 000, returns 200000
    /// </summary>
    public double Scale
    {
      get
      {
        if (_scale == 0)
          try
          {
            _scale = 1.0 / ((this.Width * _metersPerDisplayPaperUnit) / (this.ProjectionExtent.Width * _metersPerSrsStorageUnit));
          }
          catch { _scale = 0; }

        return 1.0 / _scale;
      }

      set 
      {
        if (Scale <= 0)
          throw new ArgumentException("invalid scale " + value.ToString());

        _scale = (1.0 / value);

        this.adjustMap();
      }
    }




    protected AxisList _OutputRenderingAxes;

    /// <summary>
    /// defines map specific cartesian coordinate system axes,
    /// there must be always 2 axes with name X and Y and must have direction specified
    /// as north, south, east, west
    /// </summary>
    public AxisList OutputRenderingAxes 
    {
      get { return _OutputRenderingAxes; }
      set 
      {

        Axis X = getAxis(value, "X");
        Axis Y = getAxis(value, "Y");

        //// validate num of axes
        //if (value.Count != 2)
        //  throw new ArgumentException("2 axes must be supplied");

        if(X == null || Y == null)
          throw new ArgumentException("must be 2 axes named by X and Y");

        // validate Orientation
        if (X.Orientation == Y.Orientation)
           throw new ArgumentException("invalid axes orientation");
         
        //todo:validate not x.west, y.east...

        _OutputRenderingAxes = value;

        //call adjust to update map transform
        adjustMap();
      }
    }





    /// <summary>
    /// adjusts map dimensions, scale, bounds and display transform 
    /// of <c>BaseMap</c> display region (viewport) along current map center
    /// </summary>
    protected virtual void adjustMap()
    {

      //check if all mandatory input values are valid
      if (_extent == null || _width <= 0 || _height <= 0 ||
        this.ProjectedCoordinateSystem == null || this.DisplayPaperUnits == null ||
        _transMatrix == null || _OutputRenderingAxes == null)
        return;

      //double lastScale = this.Scale;

      double WrldWidth = this.ProjectionExtent.Width;
      double WrldHeight = this.ProjectionExtent.Height;

      //double ScaleX = Math.Abs((this.Width * _metersPerDisplayPaperUnit) / (WrldWidth * _metersPerSrsStorageUnit));
      // rather use scaleY because if projection unit is projectedDegree
      // then vertical angle difference gives usually true meter size...
      double ScaleY = Math.Abs((this.Height * _metersPerDisplayPaperUnit) / (WrldHeight * _metersPerSrsStorageUnit));

      double ratioX = Math.Abs(WrldWidth / this.Width);
      double ratioY = Math.Abs(WrldHeight / this.Height);

      if (ratioX == 0 || ratioY == 0)
        throw new ApplicationException("invalid map size or extent");

      // save old map extent dimensions
      double OldWrldWidth = WrldWidth;
      double OldWrldHeight = WrldHeight;


      if (_scale != ScaleY && _scale > 0.0)
      {
        //_scale = ScaleX;
        WrldWidth = ((this.Width * _metersPerDisplayPaperUnit) / _scale) / _metersPerSrsStorageUnit;
        WrldHeight = ((this.Height * _metersPerDisplayPaperUnit) / _scale) / _metersPerSrsStorageUnit;
      }
      else
      {
        WrldWidth = this.Width * ratioX;
        WrldHeight = this.Height * ratioX;
      }

      this.ProjectionExtent.Left -= ((WrldWidth - OldWrldWidth) / 2);
      this.ProjectionExtent.Bottom -= ((WrldHeight - OldWrldHeight) / 2);

      this.ProjectionExtent.Right += ((WrldWidth - OldWrldWidth) / 2);
      this.ProjectionExtent.Top += ((WrldHeight - OldWrldHeight) / 2);

      // resolve transform by axes definition !!
      //if (_mapCRS.Unit is AngularUnit)
      //  throw new NotImplementedException();

      //prepare render matrix
      recomputeRenderMatrix();

    }




    /// <summary>
    /// helper function, return axis from list based on its name
    /// </summary>
    /// <param name="AxList">List of axes</param>
    /// <param name="name">name of axis searched for</param>
    /// <returns>found axis or null when not found</returns>
    protected Axis getAxis(AxisList AxList, string name)
    {
      foreach (Axis a in AxList)
        if (a.Name.ToLower() == name.ToLower())
        {
          if (!(a.Orientation == Axis.OrientationType.East ||
                a.Orientation == Axis.OrientationType.West ||
                a.Orientation == Axis.OrientationType.North ||
                a.Orientation == Axis.OrientationType.South))
            throw new ArgumentException("invalid axes orientation, must be east, west, north, south");

          return a;
        }
      return null;
    }






    /// <summary>
    /// returns true when projection used for map display has axis 
    /// with North direction.
    /// </summary>
    /// <param name="mapCS">Projection</param>
    /// <returns>true when projection has North Axis</returns>
    protected bool hasNorth(ProjectedCoordinateSystem mapCS)
    {
      foreach (Axis a in mapCS.Axes)
        if (a.Orientation == Axis.OrientationType.North || a.Orientation == Axis.OrientationType.South)
          return true;

      return false;
    }




    /// <summary>
    /// this helper method prepares display transform matrix
    /// for projection x map window render transform.
    /// Transformation is prepared automatically based on
    /// specific map window and projection definition.
    /// </summary>
    protected void recomputeRenderMatrix()
    {

      //todo: if map is not in pixels update scale to precision !!
      if (this.DisplayPaperUnits != MapPaperUnit.px)
        throw new ArgumentException("sorry, not implemented now, only pixels are supported");

      // pixel size in meters in map
      //double pixelSize = MapPaperUnits.inch.MetersPerUnit / (1.0 * this.Resolution);
      // horizontal resolution of map
      //double mapHorizontalResolution = (_metersPerDisplayPaperUnit * this.Width) / pixelSize;

      // this is for pixel based map
      double mapHorizontalResolution = this.Width;
      // scale ratio
      double DispTransfScale = mapHorizontalResolution / this.ProjectionExtent.Width;

      // resolve map orientation
      if (!this.KeepNorthUp)
      {

        // update display transform matrix
        // translate to letf bottom point, 
        // but because Windows screen has 0,0 in there, so map will show up side down
        // in this case ;-)
        _transMatrix.Translate(this.ProjectionExtent.Left, this.ProjectionExtent.Bottom);
        _transMatrix.Scale(DispTransfScale, DispTransfScale);

      }
      else
      {
        // map must be always north up
        // ok cartesian must have x,y axes at least
        Axis csX = getAxis(_mapCRS.Axes, "X");
        Axis csY = getAxis(_mapCRS.Axes, "Y");
        Axis mapX = getAxis(this.OutputRenderingAxes, "X");
        Axis mapY = getAxis(this.OutputRenderingAxes, "Y");

        double xtransEdge = 0, ytransEdge = 0;
        double xScale = 0, yScale = 0;
        bool axisFlip = false;

        // X axis orientation
        // if both has same orientation, then translate by left edge, no scaling x
        if ((mapX.Orientation == Axis.OrientationType.East && csX.Orientation == Axis.OrientationType.East) ||
            (mapX.Orientation == Axis.OrientationType.West && csX.Orientation == Axis.OrientationType.West))
        {
          xtransEdge = this.ProjectionExtent.Left;
          xScale = 1.0;
        }
        // if oposite orientation use right edge * -1
        else if ((mapX.Orientation == Axis.OrientationType.East && csX.Orientation == Axis.OrientationType.West) ||
                 (mapX.Orientation == Axis.OrientationType.West && csX.Orientation == Axis.OrientationType.East))
        {
          xtransEdge = this.ProjectionExtent.Right;
          xScale = -1.0;
        }


        // Y axis orientation
        // if both same use bottom edge * 1
        if ((mapY.Orientation == Axis.OrientationType.North && csY.Orientation == Axis.OrientationType.North) ||
            (mapY.Orientation == Axis.OrientationType.South && csY.Orientation == Axis.OrientationType.South))
        {
          ytransEdge = this.ProjectionExtent.Bottom;
          yScale = 1.0;
        }
        // if oposite use top edge * -1
        else if ((mapY.Orientation == Axis.OrientationType.South && csY.Orientation == Axis.OrientationType.North) ||
                 (mapY.Orientation == Axis.OrientationType.North && csY.Orientation == Axis.OrientationType.South))
        {
          ytransEdge = this.ProjectionExtent.Top;
          yScale = -1.0;
        }

        if (xScale == 0 || yScale == 0)
          throw new NotImplementedException("sorry, MapBase currently doesnt support"+
                                            "display transform that requires axis flip (x<->y)");

        //// if still not resolved...
        //if (xScale == 0 || yScale == 0)
        //{
        //  // looks like weird cs def...
        //  if (mapX.Orientation == AxisOrientationType.East && mapY.Orientation == AxisOrientationType.South &&
        //      csX.Orientation == AxisOrientationType.South && csY.Orientation == AxisOrientationType.West)
        //  {
        //    axisFlip = true;
        //    xtransEdge = this.ProjectionExtent.Left;
        //    xScale = -1.0;
        //    ytransEdge = this.ProjectionExtent.Bottom;
        //    yScale = -1.0;
        //  }
        //  else
        //    throw new ApplicationException("sorry, unsupported axis configuration");
        //}

        // update display transform matrix
        // translate to letf top point, because Windows screen has 0,0 in there
        _transMatrix.Translate(xtransEdge, ytransEdge);
        _transMatrix.Scale(xScale * DispTransfScale, yScale * DispTransfScale);
        _transMatrix.FlipAxes = axisFlip;
      }

    }


  }
}
